24.外部化配置

Spring Boot允许将配置外部化(externalize),这样你就能够在不同的环境下使用相同的代码。你可以使用properties文件,YAML文件,环境变量和命令行参数来外部化配置。使用@Value注解,可以直接将属性值注入到beans中,然后通过Spring的Environment抽象或通过@ConfigurationProperties绑定到结构化对象来访问。

Spring Boot设计了一个非常特别的PropertySource顺序,以允许对属性值进行合理的覆盖,属性会以如下的顺序进行设值:

  1. home目录下的devtools全局设置属性~/.spring-boot-devtools.properties,如果devtools激活)。
  2. 测试用例上的@TestPropertySource注解。
  3. 测试用例上的@SpringBootTest#properties注解。
  4. 命令行参数
  5. 来自SPRING_APPLICATION_JSON的属性(环境变量或系统属性中内嵌的内联JSON)。
  6. ServletConfig初始化参数。
  7. ServletContext初始化参数。
  8. 来自于java:comp/env的JNDI属性。
  9. Java系统属性(System.getProperties())。
  10. 操作系统环境变量。
  11. RandomValuePropertySource,只包含random.*中的属性。
  12. 没有打进jar包的Profile-specific应用属性application-{profile}.properties和YAML变量)。
  13. 打进jar包中的Profile-specific应用属性application-{profile}.properties和YAML变量)。
  14. 没有打进jar包的应用配置(application.properties和YAML变量)。
  15. 打进jar包中的应用配置(application.properties和YAML变量)。
  16. @Configuration类上的@PropertySource注解
  17. 默认属性(使用SpringApplication.setDefaultProperties指定)。

下面是具体的示例,假设你开发一个使用name属性的@Component

import org.springframework.stereotype.*
import org.springframework.beans.factory.annotation.*

@Component
public class MyBean {
    @Value("${name}")
    private String name;
    // ...
}

你可以将一个application.properties放到应用的classpath下,为name提供一个合适的默认属性值。当在新的环境中运行时,可以在jar包外提供一个application.properties覆盖name属性。对于一次性的测试,你可以使用特定的命令行开关启动应用(比如,java -jar app.jar --name="Spring")。

SPRING_APPLICATION_JSON属性可以通过命令行的环境变量设置,例如,在一个UNIX shell中可以这样:

$ SPRING_APPLICATION_JSON='{"foo":{"bar":"spam"}}' java -jar myapp.jar

本示例中,如果是Spring Environment,你可以以foo.bar=spam结尾;如果在一个系统变量中,可以提供作为spring.application.json的JSON字符串:

$ java -Dspring.application.json='{"foo":"bar"}' -jar myapp.jar

或命令行参数:

$ java -jar myapp.jar --spring.application.json='{"foo":"bar"}'

或作为一个JNDI变量java:comp/env/spring.application.json

24.1. 配置随机值

在注入随机值(比如,密钥或测试用例)时RandomValuePropertySource很有用,它能产生整数,longs或字符串,比如:

my.secret=${random.value}
my.number=${random.int}
my.bignumber=${random.long}
my.number.less.than.ten=${random.int(10)}
my.number.in.range=${random.int[1024,65536]}

random.int*语法是OPEN value (,max) CLOSE,此处OPEN,CLOSE可以是任何字符,并且value,max是整数。如果提供max,那么value是最小值,max是最大值(不包含在内)。

24.2. 访问命令行属性

默认情况下,SpringApplication会将所有命令行配置参数(以'--'开头,比如--server.port=9000)转化成一个property,并将其添加到Spring Environment中。正如以上章节提过的,命令行属性总是优先于其他属性源。

如果不想将命令行属性添加到Environment,你可以使用SpringApplication.setAddCommandLineProperties(false)来禁用它们。

24.3. Application属性文件

SpringApplication将从以下位置加载application.properties文件,并把它们添加到Spring Environment中:

  1. 当前目录下的/config子目录。
  2. 当前目录。
  3. classpath下的/config包。
  4. classpath根路径(root)。

该列表是按优先级排序的(列表中位置高的路径下定义的属性将覆盖位置低的)。

你可以使用YAML('.yml')文件替代'.properties'。

如果不喜欢将application.properties作为配置文件名,你可以通过指定spring.config.name环境属性来切换其他的名称,也可以使用spring.config.location环境属性引用一个明确的路径(目录位置或文件路径列表以逗号分割)。

$ java -jar myproject.jar --spring.config.name=myproject

$ java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties

在初期需要根据spring.config.namespring.config.location决定加载哪个文件,所以它们必须定义为environment属性(通常为OS env,系统属性或命令行参数)。

如果spring.config.location包含目录(相对于文件),那它们应该以/结尾(在被加载前,spring.config.name关联的名称将被追加到后面,包括profile-specific的文件名)。spring.config.location下定义的文件使用方法跟往常一样,没有profile-specific变量支持的属性,将被profile-specific的属性覆盖。

不管spring.config.location配置什么值,默认总会按照classpath:,classpath:/config,file:,file:config/的顺序进行搜索,优先级由低到高,也就是file:config/获胜。如果你指定自己的位置,它们会优先于所有的默认位置(locations),并使用相同的由低到高的优先级顺序。那样,你就可以在application.properties为应用设置默认值,然后在运行的时候使用不同的文件覆盖它,同时保留默认配置。

如果使用环境变量而不是系统属性,需要注意多数操作系统的key名称不允许以句号分割(period-separated),但你可以使用下划线(underscores)代替(比如,使用SPRING_CONFIG_NAME代替spring.config.name)。

如果应用运行在容器中,那么JNDI属性(java:comp/env)或servlet上下文初始化参数可以用来代替环境变量或系统属性,当然也可以使用环境变量或系统属性。

24.4. Profile-specific属性

除了application.properties文件,profile-specific属性也能通过命名惯例application-{profile}.properties定义。Environment(Spring的环境抽象接口)有个默认profiles集合(默认情况为[default]),在没有设置激活的profiles时会被使用(例如,如果没有明确指定激活的profiles,application-default.properties中的属性会被加载)。

Profile-specific属性加载路径和标准的application.properties相同,并且profile-specific文件总是会覆盖non-specific文件,不管profile-specific文件是否被打包到jar中。

如果定义多个profiles,最后一个将获胜。例如,spring.profiles.active定义的profiles被添加到通过SpringApplicationAPI定义的profiles后面,因此优先级更高。

如果你已经在spring.config.location下定义所有文件(非目录),那些profile-specific的文件将不被考虑。如果想使用profile-specific属性,那就在spring.config.location下使用目录。

24.5. 属性占位符

当使用application.properties定义的属性时,Spring会先通过已经存在的Environment查找该属性,所以你可以引用事先定义的值(比如,系统属性):

app.name=MyApp
app.description=${app.name} is a Spring Boot application

你也可以使用该技巧为存在的Spring Boot属性创建'短'变量,具体参考Section 69.4, “Use ‘short’ command line arguments”

24.6. 使用YAML代替Properties

YAML是JSON的一个超集,也是一种方便的定义层次配置数据的格式。只要你将SnakeYAML 库放到classpath下,SpringApplication就会自动支持YAML,以作为properties的替换。

如果你使用'Starters',添加spring-boot-starter依赖会自动加载SnakeYAML。

24.6.1. 加载YAML

Spring框架提供两个便利的类用于加载YAML文档,YamlPropertiesFactoryBean会将YAML加载为PropertiesYamlMapFactoryBean会将YAML加载为Map

例如,下面的YAML文档:

environments:
    dev:
        url: http://dev.bar.com
        name: Developer Setup
    prod:
        url: http://foo.bar.com
        name: My Cool App

会被转化到这些属性:

environments.dev.url=http://dev.bar.com
environments.dev.name=Developer Setup
environments.prod.url=http://foo.bar.com
environments.prod.name=My Cool App

YAML列表被表示成使用[index]间接引用作为属性keys的形式,例如下面的YAML:

my:
   servers:
       - dev.bar.com
       - foo.bar.com

将会转化到这些属性:

my.servers[0]=dev.bar.com
my.servers[1]=foo.bar.com

使用Spring DataBinder工具集绑定这些属性(这是@ConfigurationProperties做的事)时,你需要确保目标bean有个java.util.ListSet类型的属性,并且需要提供一个setter或使用可变的值初始化它,比如,下面的代码将绑定上面的属性:

@ConfigurationProperties(prefix="my")
public class Config {
    private List<String> servers = new ArrayList<String>();
    public List<String> getServers() {
        return this.servers;
    }
}

24.6.2. 在Spring环境中使用YAML暴露属性

YamlPropertySourceLoader类能够将YAML作为PropertySource导出到Sprig Environment,这允许你使用常用的@Value注解配合占位符语法访问YAML属性。

24.6.3. Multi-profile YAML文档

你可以在单个文件中定义多个特定配置(profile-specific)的YAML文档,并通过spring.profiles标示生效的文档,例如:

server:
    address: 192.168.1.100
---
spring:
    profiles: development
server:
    address: 127.0.0.1
---
spring:
    profiles: production
server:
    address: 192.168.1.120

在以上例子中,如果development profile被激活,server.address属性将是127.0.0.1;如果developmentproduction profiles没有启用,则该属性的值将是192.168.1.100

在应用上下文启动时,如果没有明确指定激活的profiles,则默认的profiles将生效。所以,在下面的文档中我们为security.user.password设置了一个值,该值只在"default" profile中有效:

server:
  port: 8000
---
spring:
  profiles: default
security:
  user:
    password: weak

然而,在这个示例中,由于没有关联任何profile,密码总是会设置,并且如果有必要的话可以在其他profiles中显式重置:

server:
  port: 8000
security:
  user:
    password: weak

通过!可以对spring.profiles指定的profiles进行取反(negated,跟java中的!作用一样),如果negated和non-negated profiles都指定一个单一文件,至少需要匹配一个non-negated profile,可能不会匹配任何negated profiles。

24.6.4. YAML缺点

YAML文件不能通过@PropertySource注解加载,如果需要使用该方式,那就必须使用properties文件。

24.6.5 合并YAML列表

正如上面看到的,所有YAML最终都转换为properties,在通过一个profile覆盖"list"属性时这个过程可能不够直观(counter intuitive)。例如,假设有一个MyPojo对象,默认它的namedescription属性都为null,下面我们将从FooProperties暴露一个MyPojo对象列表(list):

@ConfigurationProperties("foo")
public class FooProperties {

    private final List<MyPojo> list = new ArrayList<>();

    public List<MyPojo> getList() {
        return this.list;
    }

}

考虑如下配置:

foo:
  list:
    - name: my name
      description: my description
---
spring:
  profiles: dev
foo:
  list:
    - name: my another name

如果dev profile没有激活,FooProperties.list将包括一个如上述定义的MyPojo实体,即使dev生效,该list仍旧只包含一个实体(name值为my another namedescription值为null)。此配置不会向该列表添加第二个MyPojo实例,也不会对该项进行合并。

当一个集合定义在多个profiles时,只使用优先级最高的:

foo:
  list:
    - name: my name
      description: my description
    - name: another name
      description: another description
---
spring:
  profiles: dev
foo:
  list:
     - name: my another name

在以上示例中,如果dev profile激活,FooProperties.list将包含一个MyPojo实体(name值为my another namedescription值为null)。

23.7. 类型安全的配置属性

使用@Value("${property}")注解注入配置属性有时会比较麻烦(cumbersome),特别是需要使用多个properties,或数据本身有层次结构。Spring Boot提供一种使用配置的替代方法,这种方法允许强类型的beans以管理和校验应用的配置,例如:

@Component
@ConfigurationProperties(prefix="connection")
public class ConnectionSettings {
    private String username;
    private InetAddress remoteAddress;
    // ... getters and setters
}

添加setter和getter是相当正确的,因为绑定是通过标准的Java Beans属性描述符进行的,跟Spring MVC一样,对于不可变类型或从String强制转换的也一样。只要它们初始化了,maps,collections,arrays只需要getter,setter不是必须的,因为绑定者(binder)能够改变它们。如果有setter,maps,collections,arrays就能够被创建。Maps和collections可以仅通过getter进行扩展,而arrays需要setter。嵌套的POJO属性只能通过默认的构造器,或接收一个单一的能够转换为string的值的构造器。有些人使用Project Lombok自动添加getters和setters。

查看@Value和@ConfigurationProperties之间的区别。

你需要在@EnableConfigurationProperties注解中列出要注册的属性类:

@Configuration
@EnableConfigurationProperties(ConnectionProperties.class)
public class MyConfiguration {
}

@ConfigurationProperties bean以这种方式注册时,该bean将有个约定的名称:<prefix>-<fqn><prefix>@ConfigurationProperties注解中定义的environment key前缀,<fqn>是bean的全限定名。如果注解中没有提供任何前缀,那就只使用bean的全限定名。上述示例中的bean名称将是connection-com.example.ConnectionProperties,假定ConnectionProperties位于com.example包下。

尽管上述配置为ConnectionProperties创建了一个常规的bean,不过我们建议@ConfigurationProperties只用来处理environment(只用于注入配置,系统环境之类的),特别是不要注入上下文中的其他beans。话虽如此,@EnableConfigurationProperties注解会自动应用到你的项目,任何存在的,注解@ConfigurationProperties的bean将会从Environment属性中得到配置。只要确定ConnectionProperties是一个已存在的bean,MyConfiguration就可以不用了。

@Component
@ConfigurationProperties(prefix="connection")
public class ConnectionProperties {

    // ... getters and setters

}

这种配置风格跟SpringApplication的外部化YAML配置配合的很好:

# application.yml

connection:
    username: admin
    remoteAddress: 192.168.1.1

# additional configuration as required

为了使用@ConfigurationProperties beans,你可以像使用其他bean那样注入它们:

@Service
public class MyService {

    private final ConnectionProperties connection;

    @Autowired
    public MyService(ConnectionProperties connection) {
        this.connection = connection;
    }

     //...

    @PostConstruct
    public void openConnection() {
        Server server = new Server();
        this.connection.configure(server);
    }

}

使用@ConfigurationProperties能够产生可被IDEs使用的元数据文件,具体参考Appendix B, Configuration meta-data

此章节翻译的不好,后续整理*

24.7.1. 第三方配置

@ConfigurationProperties不仅可以注解在类上,也可以注解在public @Bean方法上,当你需要为不受控的第三方组件绑定属性时,该方法将非常有用。

为了从Environment属性中配置一个bean,你需要使用@ConfigurationProperties注解该bean:

@ConfigurationProperties(prefix = "foo")
@Bean
public FooComponent fooComponent() {
    ...
}

和上面ConnectionSettings的示例方式相同,所有以foo为前缀的属性定义都会被映射到FooComponent上。

24.7.2. Relaxed绑定

Spring Boot将Environment属性绑定到@ConfigurationProperties beans时会使用一些宽松的规则,所以Environment属性名和bean属性名不需要精确匹配。常见的示例中有用的包括虚线分割(比如,context-path绑定到contextPath),将environment属性转为大写字母(比如,PORT绑定port)。

例如,给定以下@ConfigurationProperties类:

@ConfigurationProperties(prefix="person")
public class OwnerProperties {

    private String firstName;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

}

下面的属性名都能使用:

属性 说明
person.firstName 标准驼峰规则
person.first-name 虚线表示,推荐用于.properties.yml文件中
person.first_name 下划线表示,用于.properties.yml文件的可选格式
PERSON_FIRST_NAME 大写形式,使用系统环境变量时推荐

24.7.3 属性转换

将外部应用配置绑定到@ConfigurationProperties beans时,Spring会尝试将属性强制转换为正确的类型。如果需要自定义类型转换器,你可以提供一个ConversionService bean(bean id为conversionService),或自定义属性编辑器(通过CustomEditorConfigurer bean),或自定义Converters(bean定义时需要注解@ConfigurationPropertiesBinding)。

由于该bean在应用程序生命周期的早期就需要使用,所以确保限制你的ConversionService使用的依赖。通常,在创建时期任何你需要的依赖可能都没完全初始化。

24.7.4. @ConfigurationProperties校验

Spring Boot将尝试校验外部配置,默认使用JSR-303(如果在classpath路径中),你只需要将JSR-303 javax.validation约束注解添加到@ConfigurationProperties类上:

@ConfigurationProperties(prefix="connection")
public class ConnectionProperties {

    @NotNull
    private InetAddress remoteAddress;

    // ... getters and setters

}

为了校验内嵌属性的值,你需要使用@Valid注解关联的字段以触发它的校验,例如:

@ConfigurationProperties(prefix="connection")
public class ConnectionProperties {

    @NotNull
    @Valid
    private RemoteAddress remoteAddress;

    // ... getters and setters

    public static class RemoteAddress {

        @NotEmpty
        public String hostname;

        // ... getters and setters

    }

}

你也可以通过创建一个叫做configurationPropertiesValidator的bean来添加自定义的Spring Validator@Bean方法需要声明为static,因为配置属性校验器在应用程序生命周期中创建的比较早,将@Bean方法声明为static允许该bean在创建时不需要实例化@Configuration类,从而避免了早期实例化(early instantiation)的所有问题。相关的示例可以看这里

spring-boot-actuator模块包含一个暴露所有@ConfigurationProperties beans的端点(endpoint),通过浏览器打开/configprops进行浏览,或使用等效的JMX端点,具体参考Production ready features

24.7.5 @ConfigurationProperties vs. @Value

@Value是Spring容器的一个核心特性,它没有提供跟type-safe Configuration Properties相同的特性。下面的表格总结了@ConfigurationProperties@Value支持的特性:

特性 @ConfigurationProperties @Value
Relaxed绑定 Yes No
Meta-data支持 Yes No
SpEL表达式 No Yes

如果你为自己的组件定义了一系列的配置keys,我们建议你将它们以@ConfigurationProperties注解的POJO进行分组。由于@Value不支持relaxed绑定,所以如果你使用环境变量提供属性值的话,它就不是很好的选择。最后,尽管@Value可以写SpEL表达式,但这些表达式不会处理来自Application属性文件的属性。